<?php defined('SYSPATH') or die('No direct script access.');
/**
 * Dynamic resizing of an image with option to print to screen.
 *
 * @package    DynamicImage
 * @depends	   gd2
 * @author     Sam Clark (Polaris Digital)
 * @copyright  (c) 2008 Polaris Digital
 * @licence	   GNU Public License Version 3 http://www.gnu.org/licenses/gpl.html
 * 
 * TODO Finish testing (this is an Alpha Version of 0.2.2)
 * 
 * !! NOT FOR PRODUCTION USE YET !!
 * 
 * Add - Watermarking
 * Add - Shadows
 * Add - Reflections
 * 
 */

class DynamicImage_Core {

	// Image starting properties
	protected $start_width;
	protected $start_height;

	// Image physical properties	
	protected $filename;
	protected $filesize;
	protected $mime_type;
	protected $width;
	protected $height;
	
	// GD Object In
	protected $gd_image;
	
	// GD Object Out
	protected $gd_image_out;
	protected $mime_out;
	protected $width_out;
	protected $height_out;
	
	// Crop space
	protected $crop_top;
	protected $crop_width;
	protected $crop_left;
	protected $crop_height;
	
	// Image inspection data
	protected $gd_data;
	
	// Maintain ratio
	protected $maintain_ratio;
	
	// Maintain transparency
	protected $maintain_transparency;
	
	// Background colour for jpg output
	protected $background_colour;
	
	// Configuration
	protected $config;
	
	// Confirms whether the image re-sizing is legal
	protected $legal;
	
	protected $output_sw;
	protected $output_sh;
	
	protected static $blank_png;
	
	/**
	 * 
	 * @return 
	 * @param $config Object[optional]
	 */
	public static function factory( $config = array() )
	{
		return new DynamicImage( $config );
	}
	
	/**
	 * 
	 * @return 
	 * @param $config Object[optional]
	 */
	public static function instance( $config = array() )
	{
		static $instance;
		
		empty( $instance ) and $instance = new DynamicImage( $config );
		
		return $instance;
	}	
	
	/**
	 * Standard constructor
	 * @return 
	 * @param $filename Object[optional]
	 * @param $config Object[optional]
	 */
	public function __construct( $filename = FALSE , $config = array() )
	{
		// Check for GD library before doing anything
		if( extension_loaded('gd') )
		{
			$config += Config::item('dynamicimage');
			$this->config = $config;
			
			$this->legal = FALSE;
									
			// Check for a filename and check it is a file
			if( $filename and is_file( $filename ) )
			{
				// Set filename
				$this->filename = $filename;

				$this->gd_data = getimagesize( $this->filename );
				$this->gd_image_out = FALSE;
				
				// Get filesize
				$this->filesize = filesize( $this->filename );
				
				// Get the Mimetype
				$this->mime_type = $this->gd_data['mime'];
				
				// Get dimensions				
				$this->width = $this->gd_data[0];
				$this->height = $this->gd_data[1];
				$this->start_width = $this->gd_data[0];
				$this->start_height = $this->gd_data[1];

				$this->background_colour = $this->config['background'];
				$this->maintain_transparency = $this->config['maintain_transparency'];
				$this->maintain_ratio = $this->config['maintain_ratio'];
				
				$cached_image = $this->load_cache_image();
				
				// If load cache is successful
				if( is_array( $cached_image ) )
				{
					// Print cached image
					$this->print_cached_image( $cached_image );
					
					return;
				}
				else
				{
					// Otherwise get busy resizing
					// Setup GD object
					switch( $this->gd_data['mime'] )
					{
						// If image is PNG, load PNG file
						case "image/png" : $this->gd_image = imagecreatefrompng( $this->filename ); break;
						// If image is JPG, load JPG file
						case "image/jpg" : $this->gd_image = imagecreatefromjpeg( $this->filename ); break;
						// If image is GIF, load GIF file
						case "image/gif" : $this->gd_image = imagecreatefromgif( $this->filename ); break;
						// Otherwise image is not supported in this version (more to follow)
						default : throw new Kohana_Exception("DynamicImage.__construct() Filetype {$this->mime_type} not supported yet."); return;
					}
					
					$this->process_image( $this->config['width'], $this->config['height'], $this->config['maintain_ratio'], $this->config['format'] );
				}
			}
			else
			{
				// Otherwise die horribly
				return FALSE;
			}
		}
		else
		{
			// Die informing user of lack of extentions
			throw new Kohana_Exception('GD Library not installed');
			return;
		}
	}
	
	/**
	 * Outputs the cached image
	 * @return 
	 * @param $cache_image_resource Object[optional]
	 */
	protected function print_cached_image( $cache_image_resource = FALSE )
	{
		$result = FALSE;
		// If this is a resource
		if( $cache_image_resource )
		{
			// Set the header
			header("Content-type: {$cache_image_resource['mime']}");			
						
			// Output contents
			echo $cache_image_resource['data'];
		}
				
	}
	
	protected function print_image( $width = FALSE, $height = FALSE, $maintain_ratio = 'width', $format = FALSE )
	{
		$result = FALSE;
		
		// If gd_image is valid as an gd object
		if( $this->gd_image )
		{
			// Save cache version
			$this->save_cache_image();			
			
			// Set the header
			header("Content-type: {$this->mime_out}");
			
			// Output in the desired format
			switch( $this->mime_out )
			{
				case 'image/jpeg' : imagejpeg( $this->gd_image_out, null, $this->config['compression']['jpeg'] ); imagedestroy( $this->gd_image_out ); break;
				case 'image/png' : imagealphablending($this->gd_image_out, FALSE); imagesavealpha($this->gd_image_out,true); imagepng( $this->gd_image_out, null, $this->config['compression']['png'] ); imagedestroy( $this->gd_image_out ); break;
				case 'image/gif' : imagegif( $this->gd_image_out, null ); imagedestroy( $this->gd_image_out ); break;
			}	
		}
		return;
	}
	

	/**
	 * Configures the crop settings for image output. Cropping works
	 * from width vs. height, so a ratio of 1.87 will mean that the
	 * resulting image is 1.87 times wider than it is tall.
	 * 
	 * @return (bool) TRUE if crop is perfomed, FALSE otherwise
	 */
	protected function auto_crop()
	{
		$result = FALSE;
		
		if( $this->config['auto_crop'] )
		{
			// Calculate the new image proportions
			$output_width = $this->width;
			$output_height = round($this->width / $this->config['auto_crop']);
				
			// Calculate difference in size from existing height to new height
			$scale_ratio = ( $this->height / $output_height );
				
			if( $scale_ratio < 1 )
			{
				
				// Set the crop width and height
				$this->crop_width = round( $this->width * $scale_ratio );
				$this->crop_height = $this->height;
			
				// Apply the scaling ratio to the crop_left
				$this->crop_top = round( ( $this->start_height - $this->crop_height ) / 2 );

				// Calculate height centre
				$this->crop_left = round( ( $this->start_width - $this->crop_width ) / 2 );
				
				$this->height = $output_height;								
				
				$result = TRUE;
					
			}
			else
			{

				// Set the crop width and height
				$this->crop_width = $this->width;
				$this->crop_height = round( $this->height / $scale_ratio);
				
				// Apply the scaling ratio to the crop_left
				$this->crop_top = round( ( $this->start_height - $this->crop_height ) / 2 );

				// Calculate height centre
				$this->crop_left = round( ( $this->start_width - $this->crop_width ) / 2 );
				
				$this->height = $output_height;							
			
				$result = TRUE;
			}
		}
		else
		{	
			// Set to defaults
			$this->crop_height = $this->height;
			$this->crop_width = $this->width;
			$this->crop_left = 0;
			$this->crop_top = 0;
			
			return TRUE;
		}
				
		// Check for legality of max resolution
		if( $this->config['max_resolution'] and $this->crop_width <= $this->config['max_resolution'] and $this->crop_height <= $this->config['max_resolution'] )
		{
			$this->legal = TRUE;
		}
		elseif( !$this->config['max_resolution'] )
		{
			$this->legal = TRUE;
		}
		
		$cropped_image = $this->create_blank_image( $this->crop_width, $this->crop_height );
		
		if( $state = imagecopyresampled($cropped_image, $this->gd_image, 0, 0, $this->crop_left, $this->crop_top, $this->crop_width, $this->crop_height, $this->crop_width, $this->crop_height ) )
		{
			imagedestroy( $this->gd_image );
		
			$this->gd_image = $cropped_image;
			
			
		}
		
		$this->width = imagesx( $this->gd_image );
		$this->height = imagesy( $this->gd_image );
		
		
		return $state;
	}

	
	/**
	 * Resizes the image using ratio (width defaults) if required. Will check for valid width/height
	 * @return 
	 */
	protected function resize()
	{
		$result = FALSE;
		
		$legal = FALSE;
		
		// If gd_image is valid and there is a height and width set
		if( $this->gd_image )
		{
			// If there is a width or height specified
			if( $this->maintain_ratio === 'width' )
			{
				// Figure out the width transform ratio
				$transform_ratio = $this->width_out / $this->width;
				// Apply the transform ration to the height
				$this->height_out = round( ( $this->height * $transform_ratio ), 0 );
			}
			elseif( $this->maintain_ratio === 'height' )
			{
				// Figure out the width transform ratio
				$transform_ratio = $this->height_out / $this->height;
				// Apply the transform ration to the height
				$this->width_out = round( ( $this->width * $transform_ratio ), 0 );				
			}
			else
			{
			// Otherwise leave as they are
				$this->width_out = $this->width;
				$this->height_out = $this->height;				
			}

			$this->ouput_sw = round( $this->width_out * ( $this->width_out / $this->start_width ) );
			$this->ouput_sh = round( $this->height_out * ( $this->height_out / $this->start_height ) );

			// Check legality of width and height
			if( $this->width_out > 0 and $this->height_out > 0 )
			{
				if( $this->config['max_resolution'] and $this->width_out <= $this->config['max_resolution'] and $this->height_out <= $this->config['max_resolution'] )
				{
					$this->legal = TRUE;
					$result = TRUE;
				}
				elseif( !$this->config['max_resolution'] )
				{
					$this->legal = TRUE;
					$result = TRUE;
				}
			}
		}		
				
		return $result;
	}
	
	/**
	 * Performs the final process on the image before output
	 * @return 
	 */
	protected function process_image( $width = FALSE, $height = FALSE, $maintain_ratio = 'width', $format = FALSE )
	{
		$result = FALSE;
		
		// If gd_image is valid as an gd object
		if( $this->gd_image )
		{
			
			// Setup the output MIME type
			if( $format == 'jpg' or $format == 'jpeg' )
			{
				$this->mime_out = 'image/jpeg';
			}
			elseif( $format == 'gif' )
			{
				$this->mime_out = 'image/gif';
			}
			elseif( $format == 'png' )
			{
				$this->mime_out = 'image/png';
			}
			else
			{
				$this->mime_out = $this->mime_type;
			}					

			// Crop
	
			if( !$this->auto_crop() )
			{
				throw new Kohana_Exception('DynamicImage.auto_crop() There was a problem cropping the image!');			
				return;
			}
	
			// Resize
			
			$this->width_out = $width ? $width : $this->width;
			$this->height_out = $height ? $height : $this->height;
			

			if( !$this->resize() )
			{
				throw new Kohana_Exception('DynamicImage.resize() There was a problem resizing the image!');			
				return;
			}



			if( $this->legal )
			{

				// Create the output image
				$this->gd_image_out = imagecreatetruecolor( $this->width_out, $this->height_out );
		
				if( $this->mime_out == 'image/jpeg' or $this->maintain_transparency == FALSE )
				{
					$this->set_bgcolour();
					imagealphablending( $this->gd_image_out, TRUE );
					imagesavealpha( $this->gd_image_out, FALSE );					
				}
				else
				{
					imagealphablending( $this->gd_image_out, FALSE );
					imagesavealpha( $this->gd_image_out, TRUE );										
				}

				
				// Copy image into
				if( !imagecopyresampled( $this->gd_image_out, $this->gd_image, 0, 0, 0, 0, $this->width_out, $this->height_out, $this->width, $this->height ) )
				{
					throw new Kohana_Exception('DynamicImage.process_image() There was a problem processing the final the image!');
				}
								
			// Output in the desired format
				$result = TRUE;					
			}	
		}	
		
		$this->print_image();
				
		return $result;
	}
	
	/**
	 * Sets the background colour of the image
	 * @return 
	 */
	protected function set_bgcolour()
	{
		if( $this->gd_image_out && is_array( $this->background_colour ) && count( $this->background_colour ) == 3 )
		{
			// Ensure the values do not go past the lower and upper limits for true colour
			foreach( $this->background_colour as $key => $val )
			{
				if( $val > 255 )
					$val = 255;
				if( $val < 0 )
					$val = 0;
				
				$this->background_colour[$key] = $val;
			}
			
			// Create the background colour
			$color = imagecolorallocate( $this->gd_image_out , $this->background_colour[0], $this->background_colour[1] , $this->background_colour[2] );

			// Fill the background
			imagefilledrectangle( $this->gd_image_out, 0, 0, ( $this->width_out - 1 ), ( $this->height_out -1 ), $color );
			return;
		}
	}
	
	/**
	 * Cleans up the filename for saving/loading from cache folder
	 * @return cleaned filename or FALSE
	 * @param $filename Object[optional]
	 */
	public static function clean_file_name( $filename = FALSE, $mime = FALSE )
	{
		// Setup result
		$result = FALSE;

		// If there is a filename
		if( $filename and $mime )
		{
			// If there are forward slashes in the name
			if( strpos( '/', $filename ) )
			{
				// Seperate the name
				$filename = explode( '/', $filename );
				// Rerverse the array for easy access to the filename
				$filename = array_reverse( $filename );
				// Set the filename to result
				$result = $filename[0];
			}
			
			// Replace suffix with correct mime type
			$filename = explode( '.', $result );
			array_pop( $filename );
			
			$result = "";
			
			foreach( $filename as $name_part )
				$result .= "{$name_part}.";
			
			$result .= self::get_mime_suffix( $mime );
		}
		
		return $result;
	}
	
	/**
	 * Loads a cached image if available and not expired. Expiry set in config['cache_prefix']
	 * @return either image content, or FALSE
	 */
	protected function load_cache_image()
	{
		// Create name prefix
		$filename_prefix = "";
		
		foreach( $this->config as $val )
		{
			if( !is_array( $val) )
			{
				if( $val == FALSE )
				{
					$val = 'false';
				}
				elseif( $val == TRUE )
				{
					$val = 'true';
				}
				
				$filename_prefix .= "{$val}_";			
			}
			else
			{
				foreach( $val as $var )
				{
					if( $val == FALSE )
					{
						$val = 'false';
					}
					elseif( $val == TRUE )
					{
						$val = 'true';
					}

					$filename_prefix .= "{$var}_";							
				}
			}
		}
		
		$filename_prefix .= "{$this->config['width']}_";
		$filename_prefix .= "{$this->config['height']}_";	
		
		$filename_prefix = sha1( $filename_prefix ); 

		$image_name = basename( $this->filename );
		
		// if an image is supplied
		$cache_filename = APPPATH . 'cache/' . $image_name . "_" . $this->config['cache_prefix'] . "{$filename_prefix}";

		$cache_mime_type = $this->get_file_mime( $cache_filename );
				
		$result = FALSE;

		
		// discover if the cache file is available
		if( is_file( $cache_filename ) )
		{
			// create a file pointer
			if( $file_pointer = fopen( $cache_filename, 'r') )
			{
				// If decoded contents is valid
				if( $decoded_file = fread( $file_pointer, filesize( $cache_filename ) ) )
				{
					$image_data = getimagesize( $this->filename );

					$result['data'] = $decoded_file;
					$result['mime'] = $image_data['mime'];
				}
				else
				{
					// Throw an error
					throw new Kohana_Exception( 'DynamicImage.load_cache_image() There was a problem trying to read the file ' . $cache_filename );			
					return;
				}
			}
		}

//		var_dump( $cache_filename );
		
//		exit();

		return $result;
	}
	
	/**
	 * Saves the rendered image to the cache
	 * @return TRUE if successful
	 */
	protected function save_cache_image()
	{
		// Create name prefix
		$filename_prefix = "";
		
		foreach( $this->config as $val )
		{
			if( !is_array( $val) )
			{
				if( $val == FALSE )
				{
					$val = 'false';
				}
				elseif( $val == TRUE )
				{
					$val = 'true';
				}
				
				$filename_prefix .= "{$val}_";			
			}
			else
			{
				foreach( $val as $var )
				{
					if( $val == FALSE )
					{
						$val = 'false';
					}
					elseif( $val == TRUE )
					{
						$val = 'true';
					}

					$filename_prefix .= "{$var}_";							
				}
			}
		}

		$filename_prefix .= "{$this->config['width']}_";
		$filename_prefix .= "{$this->config['height']}_";	
		
		$filename_prefix = sha1( $filename_prefix ); 
		
		$image_name = basename( $this->filename );
		
		// if an image is supplied
		$cache_filename = APPPATH . 'cache/' . $image_name . "_" . $this->config['cache_prefix'] . "{$filename_prefix}";
		
		$writable = FALSE;
		
		// if the cache directory is writable
		if( is_writeable( APPPATH . 'cache' ) )
		{
			// Check for write access is the file exists already
			if( is_file( $cache_filename ) )
			{
				// Check file is writable
				if( is_writeable( APPPATH . 'cache/' . $cache_filename ) )
				{
					// Set writable to true
					$writable = TRUE;
				}
			}
			else
			{
				// If there is no file, but the directory is writable, set writable to true
				$writable = TRUE;
			}
		}
		
		if( $writable )
		{	
			// Save the image in the correct format
			switch( $this->mime_out )
			{
				case 'image/jpeg' : imagejpeg( $this->gd_image_out, $cache_filename, $this->config['compression']['jpeg'] ); break;
				case 'image/png' : imagealphablending($this->gd_image_out, FALSE); imagesavealpha($this->gd_image_out,true); imagepng( $this->gd_image_out, $cache_filename, $this->config['compression']['png'] ); break;
				case 'image/gif' : imagegif( $this->gd_image_out, $cache_filename ); break;
			}	
		}
		else
		{
			// Throw an error
			throw new Kohana_Exception( 'DynamicImage.save_cache_image() There was a problem trying to save to the cache!' );			
			return;
		}
	}
	
	protected function create_cache_stamp( $timestamp = FALSE )
	{
		// If there is no stamp
		if( !$timestamp or !ctype_digit( $timestamp ) )
		{
			// Create one from the time now
			$timestamp = time();
		}
		
		$formatted_timestamp = date( 'YMd,His' );
		
		return ":kdynimg_cache:{$formatted_timestamp}:\n";
	}
	
	protected function decode_cache_stamp( $file_contents = FALSE )
	{
		$result = FALSE;
		
		// If there is contents
		if( $file_contents )
		{
			// Setup search needle
			$search_needle = "/:kdynimg_cache:(?<year>\d{4})(?<month>\w{3})(?<day>\d{2}),(?<hour>\d{2})(?<minute>\d{2})(?<second>\d{2}):\n/";			
				
			// If the cache data is found
			if( preg_match( $search_needle, $file_contents, $cache_data ) )
			{
				$file_contents = preg_replace( $search_needle, '', $file_contents );
				
				$timestamp = strtotime( "{$cache_data['hour']}:{$cache_data['minute']}:{$cache_data['second']} {$cache_data['day']} {$cache_data['month']} {$cache_data['year']}");
					
				$result = array( 'timestamp' => $timestamp, 'clean_file' => $file_contents );
			}
		}
		return $result;
	}
	
	/**
	 * Converts a hex value to an RGB array
	 * @return 
	 * @param $hexstr Object
	 * @param $rgb Object
	 */
	public static function hexrgb($hexstr, $rgb)
	{
		 $int = hexdec($hexstr);
		 switch($rgb)
		 {
			 case "r": return 0xFF & $int >> 0x10; break;
			 case "g": return 0xFF & ($int >> 0x8); break;
			 case "b": return 0xFF & $int; break;
			 default: return array(
			            0 => 0xFF & $int >> 0x10,
			            1 => 0xFF & ($int >> 0x8),
			            2 => 0xFF & $int
			            ); break;
		}
	}
	
	/**
	 * Coverts a percentage of a size to pixels
	 * @return 
	 * @param $percent Object[optional] percentage value
	 * @param $pixel_orginal_size Object[optional] original pixel size (width or height)
	 * @param $round Object[optional] whether to round the result or not
	 */
	protected function percentage_to_pixel( $percent = FALSE, $pixel_orginal_size = FALSE, $round = TRUE )
	{
		$result = FALSE;
		
		if( $percent and $pixel_orginal_size )
		{
			$result = $round ? round( $pixel_orginal_size * ( $percent / 100 ) ) : ( $pixel_orginal_size * ( $percent / 100 ) );
		}
		return $result;
	}
	
	/**
	 * Returns the suffix for the file name, dictated by mimetype
	 * @return 
	 * @param $mime Object[optional]
	 */
	protected function get_mime_suffix( $mime = FALSE )
	{
		$result = FALSE;
		
		if( $mime )
		{
			switch( $mime )
			{
				case "image/jpeg" : $result = "jpg"; break;
				case "image/jpg" : $result = "jpg"; break;
				case "image/png" : $result = "png"; break;
				case "image/gif" : $result = "gif"; break;
			}
		}
		return $result;
	}
	
	/**
	 * Returns the suffix for the file name, dictated by mimetype
	 * @return 
	 * @param $mime Object[optional]
	 */
	protected function get_file_mime( $filename = FALSE )
	{
		$result = FALSE;
	
		$ext = array_pop( explode( ".", $filename ) );
		
		if( $ext )
		{
			switch( $ext )
			{
				case "jpg" : $result = "image/jpeg"; break;
				case "png" : $result = "image/png"; break;
				case "gif" : $result = "image/gif"; break;
			}
		}
		return $result;
	}
	
	/**
	 * Creates a blank cavas to use for resizing/cropping the new image
	 * @return GD object
	 * @param $width Object[optional]
	 * @param $height Object[optional]
	 */
	public static function create_blank_image( $width = 10, $height = 10 )
	{
		// If empty, create blank png
		if( self::$blank_png === NULL )
		{
			self::$blank_png = imagecreatefromstring( base64_decode( "iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAACXBIWXMAAAsTAAAL
EwEAmpwYAAAADElEQVQImWNgoBMAAABpAAFEI8ARAAAAAElFTkSuQmCC" ) );

			// Setup the temporary width and height from the temp image
			$temp_width 	= imagesx( self::$blank_png );
			$temp_height 	= imagesy( self::$blank_png );
		}
		
		// Create a new image
		$gd_out_img = imagecreatetruecolor( $width, $height );
		
		// Copy the blank image to the new GD object
		imagecopyresized( $gd_out_img, self::$blank_png, 0, 0, 0, 0, $width, $height, $temp_width, $temp_height );
		
		// Perserve alpha information
		imagealphablending($gd_out_img, FALSE);
		imagesavealpha($gd_out_img, TRUE);
		
		// Return the image object
		return $gd_out_img;		
	}
}